package com.gao.masterslave.service.impl;

import com.gao.masterslave.common.Constants;
import com.gao.masterslave.common.id.IdGenerator;
import com.gao.masterslave.common.req.OrderReq;
import com.gao.masterslave.common.res.OrderResult;
import com.gao.masterslave.common.res.ProcessResult;
import com.gao.masterslave.common.res.StockResult;
import com.gao.masterslave.mapper.ProductMapper;
import com.gao.masterslave.po.Product;
import com.gao.masterslave.service.OrderService;
import com.gao.masterslave.util.RedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.Map;

@Service("OrderServiceImpl")
public class OrderServiceImpl implements OrderService {

    private Logger logger = LoggerFactory.getLogger(OrderService.class);

    @Autowired
    RedisUtil redisUtil;

    @Resource
    private ProductMapper productMapper;

    @Resource
    private Map<Constants.Ids, IdGenerator> idGeneratorMap;

    /**
     * 订单处理流程
     * @param orderReq
     * @return
     */
    @Override
    public ProcessResult doProcess(OrderReq orderReq) {
        // 订单创建
        OrderResult orderResult = createOrder(orderReq);
        if (!Constants.ResponseCode.SUCCESS.getCode().equals(orderResult.getCode())) {
            return new ProcessResult(orderResult.getCode(), orderResult.getInfo());
        }
        // 更新产品库存，应该异步去处理数据库库存，这里简单处理下
        productMapper.updateProductStock(orderResult);
        return new ProcessResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo());
    }

    /**
     * 创建订单
     * @return
     */
    public OrderResult createOrder(OrderReq req) {
        // 1、查询产品库存信息
        Product productInfo = productMapper.queryProductById(req.getProductId());
        // 2、扣减库存，加锁
        StockResult stockResult = deductStockByRedisLua(req.getUserId(), req.getProductId(), productInfo.getStockCount());
        if (!Constants.ResponseCode.SUCCESS.getCode().equals(stockResult.getCode())) {
            // 扣减库存失败，释放锁
            recoverProductCacheStockByRedis(req.getProductId(), stockResult.getStockKey(), stockResult.getCode());
            return new OrderResult(stockResult.getCode(), stockResult.getInfo());
        }
        // 3、生成订单号
        Long orderNo = idGeneratorMap.get(Constants.Ids.SnowFlake).nextId();
        // 4、订单创建完毕，释放库存锁
        recoverProductCacheStockByRedis(req.getProductId(), stockResult.getStockKey(), Constants.ResponseCode.SUCCESS.getCode());
        // 5、返回结果
        OrderResult orderResult = new OrderResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo());
        orderResult.setOrderNo(orderNo);
        orderResult.setProductId(productInfo.getProductId());
        orderResult.setStockSurplusCount(stockResult.getStockSurplusCount());

        return orderResult;
    }

    /**
     * 扣减库存
     * @param uId          用户ID
     * @param productId    产品ID
     * @param stockCount   库存数量
     * @return
     */
    public StockResult deductStockByRedis(Long uId, Long productId, Integer stockCount) {
        // 1、获取产品的库存 key
        String stockKey = Constants.RedisKey.getProductStockCount(productId);
        // 2、扣减库存，返回当前库存已用数量
        Integer stockUsedCount = (int) redisUtil.incr(stockKey, 1);
        // 3. 如果超出库存，恢复原始库存，返回失败结果
        if (stockUsedCount > stockCount) {
            // 恢复库存
            redisUtil.decr(stockKey, 1);
            logger.info("{}库存不足：", productId);
            return new StockResult(Constants.ResponseCode.OUT_OF_STOCK.getCode(), Constants.ResponseCode.OUT_OF_STOCK.getInfo());
        }
        // 4. 以产品库存占用编号，生成对应加锁库存Key，细化锁的颗粒度
        String stockCountLockKey = Constants.RedisKey.getProductStockCountLock(productId, stockUsedCount);
        // 5. 使用 Redis.setNx 加一个分布式锁
        boolean isLocked = redisUtil.setNx(stockCountLockKey, 500L);
        if (!isLocked) {
            logger.info("用户{}购买产品{}，分布式锁失败：{}", uId, productId, stockCountLockKey);
            return new StockResult(Constants.ResponseCode.ERR_TOKEN.getCode(), Constants.ResponseCode.ERR_TOKEN.getInfo());
        }

        return new StockResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo(), stockCountLockKey, stockCount - stockUsedCount);
    }

    /**
     * Redis Lua 脚本
     * @param uId
     * @param productId
     * @param stockCount
     * @return
     */
    public StockResult deductStockByRedisLua(Long uId, Long productId, Integer stockCount) {
        // 设置随机数的value
        long value = idGeneratorMap.get(Constants.Ids.RandomNumeric).nextId();
        String productStockRandomLock = Constants.RedisKey.getProductStockRandomLock(value);
        boolean set = redisUtil.set(productStockRandomLock, value);
        // 1、获取产品的库存 key
        String stockKey = Constants.RedisKey.getProductStockCount(productId);
        // 2、扣减库存，返回当前库存已用数量
        Integer stockUsedCount = (int) redisUtil.incr(stockKey, 1);
        // 3. 如果超出库存，恢复原始库存，返回失败结果
        if (stockUsedCount > stockCount) {
            // 恢复库存
            redisUtil.decr(stockKey, 1);
            logger.info("{}库存不足：", productId);
            return new StockResult(Constants.ResponseCode.OUT_OF_STOCK.getCode(), Constants.ResponseCode.OUT_OF_STOCK.getInfo());
        }
        Long lockValue = (Long) redisUtil.get(productStockRandomLock);
        // 4. Lua 脚本 释放锁
        if (lockValue != value) {
            logger.info("用户{}购买产品{}，分布式锁失败：{}", uId, productId, stockKey);
            releaseLock(productStockRandomLock, value);
            return new StockResult(Constants.ResponseCode.ERR_TOKEN.getCode(), Constants.ResponseCode.ERR_TOKEN.getInfo());
        }

        return new StockResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo(), productStockRandomLock, stockCount - stockUsedCount);
    }

    /**
     * 释放锁
     * @param productId
     * @param tokenKey
     * @param code
     */
    public void recoverProductCacheStockByRedis(Long productId, String tokenKey, String code) {
        if (null == tokenKey) {
            return;
        }
        // 删除分布式锁 Key
        redisUtil.del(tokenKey);
    }

    public void releaseLock(String key, Long value) {
        //设置什么类型，返回类型就是什么类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setResultType(Long.class);
        //lua文件存放在resources目录下的redis文件夹内
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redislua.lua")));
        redisUtil.excute(redisScript, Arrays.asList(key), value);
    }
}
